package Gameplay;
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.MouseInfo;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.ArrayList;

import ADTs.LevelStateADT;
import ADTs.PointADT;
import Gameplay.Enemy.AI;
import Gameplay.Enemy.Enemy;
import Gameplay.Projectile.Projectile;
import Project.Constants;
import Project.Main;
import Project.Menu;
import Project.MyGraphics;
import Project.SaveFile;
import Project.UI;

/**
 * A class that allows a user to play a level. Contains a list 
 * of objects on screen and while running, will continually 
 * draw them frameRate times per second.
 * 
 * @author	Brian Wetherbee
 * 			Ryan McNamara
 * 			Zeng Yang
 * 
 */
public class Level extends UI implements Runnable {
	private /*@ spec_public @*/int levelNumber; //current level number
	private /*@ spec_public @*/LevelStateADT state; //current state in the level
	private /*@ spec_public @*/int waveNumber; //current wave number
	private /*@ spec_public @*/ArrayList<Wave> waves; //all the waves in the current level
	private /*@ spec_public @*/int frameRate; 
	private /*@ spec_public @*/int frameDuration;
	private /*@ spec_public @*/Thread gameplay;
	private /*@ spec_public @*/Thread menu;
	private /*@ spec_public @*/int drawState; //0: starting, 1: normal, 2: you lost, 3: you won, 4: quitting
	private /*@ spec_public @*/boolean levelInProgress;
	private /*@ spec_public @*/boolean isIT; //Check if current level is IT level
	public static final int barHeight = 64;
	public static final double itEnemySpeed = 2.5;
	
	/*@ public invariant levelNumber <= 27 && levelNumber >= 0;
	  @ public invariant state != null;
	  @ public invariant state.wave != null;
	  @ public invariant waveNumber >= 0;
	  @ public invariant frameRate > 0;
	  @ public invariant frameDuration > 0;
	  @ public invariant drawState >= 0 && drawState <= 4;
	  @*/
	
	
	
	//@ requires levelno >= 0 and levelno < 27;
	//@ requires prevUI != null;
	/*@ ensures this.levelNumber == levelno;
	  @ ensures this.previousUI = preUI;
	  @ ensures this.isIT = isIT;
	 @*/
	public Level(int levelno, UI prevUI, boolean isIT){
		
		this.isIT = isIT;
		this.previousUI = prevUI;
		
		levelNumber = levelno;
		initComponents();//add key and mouse listeners, and set the bounds of current level.
		repaint();
		this.setFrameRate(50);//initialize the FrameRate before starting a level.
		this.drawState = 0;//initialize the number of drawState (0 -> starting).
		System.out.println("level number is:" + levelNumber);
		
		
	}
	//@ requires state.getPlayer() != null;
	//@ ensures waves.size() == LevelConstants.setUpWaves(levelNumber).size();
	//@ ensures state.getPlayer().movementBounds.height == Constants.height - Level.barHeight;
	//@ ensures state.getPlayer().movementBounds.width == Constants.width/2;
	/*@ ensures (\forall int i; i >= 0 && i < waves.size(); 
				waves.get(i).equals(LevelConstants.setUpWaves(levelNumber).get(i)));
				@*/
	public void startLevel(){
		state = new LevelStateADT();
		//to setup a different movementBounds if the player is in IT level.
		if(isIT){
			state.getPlayer().movementBounds = new Rectangle(0,0,Constants.width / 2,Constants.height - Level.barHeight);
		}

		waves = LevelConstants.setUpWaves(levelNumber);// setup the waves with respect to level number.
		menu = Thread.currentThread();
		gameplay = new Thread(this);
		gameplay.start();//game starts.
		
	}
	
	/**
	 * This method is to paint all kinds of objects and quotations 
	 * when player is starting the level, playing the level and quitting
	 * the level.
	 * 
	 * @oara gg the graphics on the screen
	 */
	@Override
	public void paint(Graphics g){
		super.paint(g);
		g.setColor(Color.BLACK);// to setUp the background as color black.
		g.fillRect(0, 0,Constants.width,Constants.height);
		
		//To print "Ready?" in the middle of the screen when the player is starting a game in a level.
		if(drawState == 0){
			g.setColor(Color.red);
			g.fillRect(Constants.width / 2, Constants.height / 2, 2, 2);
			g.setColor(Color.WHITE);//setup the color of the characters.
	    	g.setFont(new java.awt.Font("DialogInput",1,50));//setup the font of the characters.
	    	FontMetrics f = g.getFontMetrics(new java.awt.Font("DialogInput",1,50));
	    	int stringX = f.stringWidth("Ready?");
	    	int stringY = f.getHeight();
	    	//setup the location of the characters on the screen.
	    	g.drawString("Ready?",Constants.width / 2 - (stringX/2), Constants.height / 2 - (stringY)/2);
		}
		//To draw everything in the objectList on the screen.
		if (drawState == 1) {
			for (int i = 0; i < state.getObjectList().size(); i++) {
				OnScreenObject o = state.getObjectList().get(i);
				o.paint(g);
			}
			drawInfoPane(g);
		}
		
		//To print "You Suck." in the middle of the screen when the player is dead.
		if(drawState == 2){
			g.setColor(Color.WHITE);//setup the color of the characters.
	    	g.setFont(new java.awt.Font("DialogInput",1,50));
	    	FontMetrics f = g.getFontMetrics(new java.awt.Font("DialogInput",1,50));//setup the font of the characters.
	    	double stringX = Main.actualToInternalX(f.stringWidth("You suck."));
	    	double stringY = Main.actualToInternalX(f.getHeight());
			//To draw everything in the objectList on the screen.
	    	g.drawString("You lost.",(int)(Constants.width / 2 - (stringX/2)),(int)( Constants.height / 2 - (stringY)/2));
		}

		//To print "You won." in the middle of the screen after the player 
		//kills all of the waves of enemies of current level.
		if(drawState == 3){
			g.setColor(Color.WHITE);//setup the color of the characters.
	    	g.setFont(new java.awt.Font("DialogInput",1,50));
	    	FontMetrics f = g.getFontMetrics(new java.awt.Font("DialogInput",1,50));//setup the font of the characters.
	    	double stringX = Main.actualToInternalX(f.stringWidth("You won."));
	    	double stringY = Main.actualToInternalX(f.getHeight());
			//To draw everything in the objectList on the screen.
	    	g.drawString("You won.",(int)(Constants.width / 2 - (stringX/2)), (int)(Constants.height / 2 - (stringY)/2));
		}
		//To print "You quit." in the middle of the screen if the player 
		//quits the game.
		if(drawState == 4){
			g.setColor(Color.WHITE);//setup the color of the characters.
	    	g.setFont(new java.awt.Font("DialogInput",1,50));
	    	FontMetrics f = g.getFontMetrics(new java.awt.Font("DialogInput",1,50));//setup the font of the characters.
	    	double stringX = Main.actualToInternalX(f.stringWidth("You quit."));
	    	double stringY = Main.actualToInternalX(f.getHeight());
			//To draw everything in the objectList on the screen.
			g.drawString("You quit.",
					(int) (Constants.width / 2 - (stringX / 2)),
					(int) (Constants.height / 2 - (stringY)/2));
		}
	}
	/**
	 * This method is to draw three panes on the screen when player is 
	 * playing in a level. The panes store the current information of 
	 * player's current hp, player's current money and player's current
	 * weapon status.
	 * 
	 * @param gg the graphics on the screen
	 */
	//@requires gg != null;
	private void drawInfoPane(Graphics g){

		/**
		 * pane 1
		 */
		//Initialize the HP bar of the player on the screen.
		g.setColor(new Color(64,64,64));
		
		//Setup size of the hp bar.
    	g.fillRect(0, Constants.height - barHeight, Constants.width, barHeight);
    	g.setColor(new Color(128,128,128));
    	g.drawRect(0, Constants.height - barHeight, Constants.width / 3, barHeight);
    	
    	//Change a proportion of the bar to color red whenever the player gets hurts
    	g.setColor(Color.RED);
    	int hbx = 1;
    	int hby = Constants.height - barHeight + 1;
    	int hbwidth= Constants.width / 3 - 1;
    	int hbheight = barHeight - 2;
    	g.fillRect(hbx,hby,hbwidth,hbheight);
    	
    	//to calculate the ration b/w. the max hp and the player's hp.
    	hbwidth *= this.getPlayer().getHP() / this.getPlayer().getMaxHP();
    	g.setColor(Color.green);
    	g.fillRect(hbx,hby,hbwidth,hbheight);
    	
    	/**
    	 * pane 2
    	 */
    	
    	//initialize background color and size of the money pane.
    	g.setColor(new Color(128,128,128));
    	g.drawRect(Constants.width / 3, Constants.height - barHeight, 2*Constants.width / 3, barHeight);

    	//get the money value from the current save file.
    	String toDraw = "$ " + (int)SaveFile.getCurrentFile().getMoney();
    	
    	//setup the all kinds of properties of the characters 
    	int fontSize = (int)(barHeight * .8);
    	g.setFont(new java.awt.Font("DialogInput",1,(fontSize)));
    	FontMetrics f = g.getFontMetrics(new java.awt.Font("DialogInput",1,(fontSize)));
    	double stringX = Main.actualToInternalX(f.stringWidth(toDraw));
    	double stringY = Main.actualToInternalX(f.getHeight());
    	int mx = (int) (Constants.width / 2 - stringX / 2.0);
    	int my = (int) (Constants.height - 1/4.0 * stringY);
    	g.drawString(toDraw,mx,my);

    	/**
    	 * pane 3
    	 */
    	//initialize the backgrond color and size of weapon status pane.
    	g.setColor(new Color(128,128,128));
    	g.drawRect(2*Constants.width / 3, Constants.height - barHeight, Constants.width, barHeight);
    	
    	//to get the name of the weapon that the player is currently using.
    	toDraw = this.getPlayer().getWeapon().getName();

    	//setup the all kinds of properties of the characters 
    	stringX = Main.actualToInternalX(f.stringWidth(toDraw));
    	mx = (int) (5*Constants.width / 6 - stringX / 2.0);
    	my = (int) (Constants.height - 1/4.0 * stringY);
    	g.drawString(toDraw,mx,my);
	}
	
	//To initialize the components of the level screen.
	public void initComponents(){
		this.setBounds(0,0,(int)Main.getActualComponentSize().getX(),(int)Main.getActualComponentSize().getY());
		addKeyListener(this);
        addMouseListener(this);
        
	}
	
	/*@
	  @ requires levelInProgress == true;
	  @ ensures levelInProgress == false;
	  @*/
	//end the level if either the player beats or loses the game
	public void endLevel(){
		levelInProgress = false;
		((Menu)previousUI).updateInformation();
		
	}
	/*@
	  @ requires ((Level)Main.getCurrentUI()).getPlayer().getHP() <= 0;
	  @ ensures drawState == 2;
	  @*/
	//end level if the player loses the level.
	public void loseLevel(){
		drawState = 2;
		endLevel();
	}
	/*@
	  @ requires ((Level)Main.getCurrentUI()).getPlayer().getHP() > 0;
	  @ ensures SaveFile.getCurrentFile().getLevelsBeaten()  >= levelNumber
	  @ ensures drawState == 3;
	  @*/
	//Increase the number of levelsBeaten if the players beats the level.
	public void beatLevel(){
		drawState = 3;
		System.out.println("levels beaten: " + SaveFile.getCurrentFile().getLevelsBeaten());
		if (SaveFile.getCurrentFile().getLevelsBeaten()  == levelNumber){
			SaveFile.getCurrentFile().incrementLevelsBeaten();
		}

		endLevel();
		
	}
	/*@
	  @ requires w != null;
	  @ requires ((Level)Main.getCurrentUI()).getPlayer().getHP() > 0;
	  @ requires w.getEnemyList != null;
	  @ ensures Game.currentUI == LocalMap.UI;
	  @*/
	//add a wave to current state.
	public void addWave(Wave w){
		for(Enemy e: w.getEnemyList())
		{
			this.addToState(e);
			System.out.println(e.location);
		}
	}
	
	@Override
	public void mouseClicked(MouseEvent e) {
		// TODO Auto-generated method stub
		
	}
	
	public PointADT getMouseLocation(){
		return Main.getMouseLocation();
	}
	
	private boolean firePressed;//boolean variable for verifying if the player is firing.
	public boolean isFirePressed(){ return firePressed; }
	
	//setup firePressed = true if the player presses a mouseEvent in the level.
	//@ requires e != null;
	//@ ensures friePresses == true;
	@Override
	public void mousePressed(MouseEvent e) {
		this.firePressed = true;
	}

	//setup firePressed = false if the player releases a mouseEvent in the level.
	//@ requires e != null;
	//@ ensures friePresses == false;
	@Override
	public void mouseReleased(MouseEvent e) {
		this.firePressed = false;		
	}

	@Override
	public void mouseEntered(MouseEvent e) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void mouseExited(MouseEvent e) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void keyTyped(KeyEvent e) {
		
		
	}

	
	
	private boolean upPressed = false;//boolean variable for verifying if the player up presses.
	public boolean isUpPressed() {return upPressed;}
	private boolean downPressed = false;//boolean variable for verifying if the player down presses.
	public boolean isDownPressed() {return downPressed;}
	private boolean leftPressed = false;//boolean variable for verifying if the player left presses.
	public boolean isLeftPressed() {return leftPressed;}
	private boolean rightPressed = false;//boolean variable for verifying if the player right presses.
	public boolean isRightPressed() {return rightPressed;}
	
	//@ requires e != null;
	@Override
	public void keyPressed(KeyEvent e) {
		int key = e.getKeyCode();//get the code of pressed key.
		//setup leftPressed = true if the key is Constants.controlLeft 
		if (key == Constants.controlLeft) {
			leftPressed = true;
		}
		//setup downPressed = true if the key is Constants.controlDown
		if (key == Constants.controlDown) {
			downPressed = true;
		}
		//setup rightPressed = true if the key is Constants.controlRight
		if (key == Constants.controlRight) {
			rightPressed = true;
		}
		//setup upPressed = true if the key is Constants.controlUp
		if (key == Constants.controlUp) {
			upPressed = true;
		}
	}

	//@ requires e != null;
	@Override
	public void keyReleased(KeyEvent e) {
		int key = e.getKeyCode();//get the code of released key.
		//setup leftPressed = false if the key is Constants.controlLeft 
		if (key == Constants.controlLeft) {
			leftPressed = false;
		}
		//setup downPressed = false if the key is Constants.controlDown
		if (key == Constants.controlDown) {
			downPressed = false;
		}
		//setup rightPressed = false if the key is Constants.controlRight
		if (key == Constants.controlRight) {
			rightPressed = false;
		}
		//setup upPressed = false if the key is Constants.controlUp
		if (key == Constants.controlUp) {
			upPressed = false;
		}
		
		//setup the proper number of drawState, and make levelInProgress = false since
		//the player is quitting the game.
		if (key == Constants.controlCancel){
			this.drawState = 4;
			this.levelInProgress = false;
		}

	}
	
	/**
	 * This method is to draw every frame includes all kinds of 
	 * objects when level is in progress. Also this method can spawn
	 * all the waves in the wave list on the screen.
	 * 
	 * @exception InterruptedException
	 */
	//@ ensures Main.getCurrentUI() != this.previousUI
	public void run() {
		this.drawState = 0;
		repaint();
		
		//to make "Ready?" be on the screen for 2 seconds so that the player 
		//has time to prepare the coming level.
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
		this.drawState = 1;
		
		long lastTime = System.currentTimeMillis();
		long nowTime = System.currentTimeMillis(); // used to affect how long loop sleeps for to create frame rate
		repaint();//getting into the level.
		levelInProgress = true;
		
		//keep changing the frames and drawing the objectList on the screen if while
		//levelInProgrss is true.
		while(levelInProgress){
			if(waveNumber == 0) {
				//break the loop, and call beatLevel() if waves have no enemies.
				if(waves.size() == 0){
					beatLevel();
					break;
				}
				//add a new wave on the screen
				addWave(waves.get(0));
				waveNumber += 1;//update the number of wave
			} else if(waves.get(waveNumber - 1).isFinished()) {
				//call beatLevel() if the player beats all the waves in the wave list.
				if(waveNumber == waves.size()) {
					beatLevel();
				} else {
					//add a new wave if the wave number hasn't reaches the wave size.
					addWave(waves.get(waveNumber));
					waveNumber += 1;//update waveNumber
				}
				
			}
			
			//get all of the objects in the objectList and make them move if they have
			//essential movement factors.
			for(int i = 0; i < state.getObjectList().size();i++){
				OnScreenObject o = state.getObjectList().get(i);
				o.move();	
			}
			
			//paint frames by this.getFrameRate()
			try {
				nowTime = System.currentTimeMillis();
				long sleeptime = Math.max(frameDuration - (nowTime - lastTime),0);
				Thread.sleep(sleeptime);
				if(sleeptime == 0){
					System.err.println("frame skip");
				}
				repaint();
				lastTime = System.currentTimeMillis();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		try {

			repaint();
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		Main.setCurrentUI(this.previousUI);
	}
	
	/*@ requires o != null;
	  @ ensures this.state.getObjectList().size()
	  @ ensures o instanceof Player ==> this.state.getPlayer() == o;
	  @ ensures o instanceof Projectile ==> this.state.getProjectileList().size() -1 == (\old this).state.getProjectileList().size();
	  @ ensures o instanceof Enemy ==> this.state.getEnemyList().size() -1 == (\old this).state.getEnemyList().size();
	 @*/
	
	//to add OnscreenObject into the current state.
	public void addToState(OnScreenObject o){
		this.state.getObjectList().add(o);//add the given object into the objectList.
		
		//setup the this.state.getPlayer() = OnScreenObject o if it is a Player object
		if(o instanceof Player){
			Player p = (Player)o;
			this.state.setPlayer(p);
		}
		//add a projectile into the projectileList of the state if it is a Projectile object
		if(o instanceof Projectile){
			Projectile p = (Projectile)o;
			this.state.getProjectileList().add(p);
		}
		//add a enemy into the enemy list of the state if it is a enemy object
		if(o instanceof Enemy){
			Enemy p = (Enemy)o;
			this.state.getEnemyList().add(p);
		}
	}
	
	/*@ requires o != null;
	  @ ensures this.state.getObjectList().contains(o) == false;
	  @ ensures o instanceof Player ==> this.state.getPlayer() == null;
	  @ ensures o instanceof Projectile ==> this.state.getProjectileList().contains(o) == false;
	  @ ensures o instanceof Enemy ==> this.state.getEnemyList()..contains(o) == false;
	 @*/
	//to remove OnscreenObject from the current state.
	public void removeFromState(OnScreenObject o){
		this.state.getObjectList().remove(o);

		//remove the enemy from a wave if it is a enemy object
		if(o instanceof Enemy) {
			waves.get(waveNumber - 1).removeEnemy((Enemy) o);
		}

		//remove the player from the state if it is a player object, and then call loseLevel()
		if(o instanceof Player){
			Player p = (Player)o;
			this.loseLevel();
		}
		//remove the projectile from the projectileList() if it is a projectile object.
		if(o instanceof Projectile){
			Projectile p = (Projectile)o;
			this.state.getProjectileList().remove(p);
		}
		//remove the enemy from the enemyList if it is a enemy object.
		if(o instanceof Enemy){
			Enemy p = (Enemy)o;
			this.state.getEnemyList().remove(p);
		}
	}
	
	/*@ requires frameRate > 0;
	  @ ensures frameDuration == 1000 / frameRate;
	  @ ensures this.frameRate = framRate;
	 @*/
	public void setFrameRate(int frameRate) {
		frameDuration = 1000 / frameRate;
		this.frameRate = frameRate;
	}
	//@ ensure \result == frameRate;
	public int getFrameRate() {
		return frameRate;
	}

	//@ requires frameDuration > 0;
	//@ ensures this. frameDuration == frameDuration;
	private void setFrameDuration(int frameDuration) {
		this.frameDuration = frameDuration;
	}

	//@ ensures \result == frameDuration 
	public int getFrameDuration() {
		return frameDuration;
	}
	
	//@ ensures \result == this.state.getPlayer();
	public Player getPlayer() {
		return this.state.getPlayer();
	}
	//To return a list of object
	public ArrayList<OnScreenObject> getObjectList() {
		return (ArrayList<OnScreenObject>) state.getObjectList().clone();
	}
	
	public boolean isIT(){
		return this.isIT;
	}
	public ArrayList<Wave> getWaves() {
		// TODO Auto-generated method stub
		return waves;
	}
	
	
	
	
}
